テスト駆動開発って何だろう
はじめに
モバイルアプリサービス部の中安です。
このたび、テスト駆動開発(Test Driven Development = TDD)の社内読書会を数ヶ月に渡って参加させていただきました。
原題: Test-Driven Development By Example 著: ケント・ベック 訳: 和田卓人
こういうものに参加するのも初めてなので「なんとかついていく」という感じでしたが、 最終的には無知識だった初めの印象から変わった部分も多く、有意義だったなぁという感想に至りました。
すでに社内読書会については素晴らしいまとめがありますので、
詳しくご覧になりたい方は下記のリンクをご覧くださいませ。
『テスト駆動開発』読書勉強会
では、自分はここに何を書いていくかというと、
- 「テスト駆動開発って何だろう」から始まった自分が読書会を通じてわかったこと
- 弊社に直接お越しいただき話をしていただいた、著訳をされた和田(t_wada)さんが話されたこと
- 和田さんの講演動画(※1)を見て、知ったことや感じたこと
これらをツラツラと書いていこうかと思います。
※1 こちらの講演動画はこのブログの最後に埋め込んでいます。
TDDの目的
テスト駆動開発はテストから書いていくという「テストファースト」な方法であることは言うまでもありません。
ただ「なぜそうするのか」「目的は何だ」というのがハッキリしていないと「ただテストを最初に書く」という順番論にしかならなのではないでしょうか。
TDDの目指すゴール。いやTDDじゃなくてもエンジニアなら誰もが目指すゴール。 それは「キレイな実装で」で「ちゃんと動く」ソースコードです。
そのゴールに到達するまでには2つの行き方があると、講演動画では解説されていました。
- キレイに設計してから/ちゃんと動くところまで持っていく
- ちゃんと動くところまで持っていってから/キレイに実装する
ふむふむ、なるほど。 たしかにこの2パターンはたしかにありますね。 人によってスタイルがあるんじゃないかと思います。 しかし、それぞれには問題が生じてしまいます。
問題点とは
キレイに設計してから/ちゃんと動くところまで持っていく の問題点
「キレイに実装」というのは、つまりは設計をガッチリやって、それをまずソースコードに清書してしまおうということです。 それは例えばアーキテクチャーからクラスデザインパターンであったり、メソッド名や引数名に至るまでです。
キレイに作られていると、エンジニアさんならばテンションが上っていくところですが、ここでふと思うわけです。
「これってちゃんと動くんですよね?」と
「行き過ぎた設計配慮なんじゃないの?」とか「動いたとしてもパフォーマンスが悪いんじゃないの?」とかそういう疑念や実証が始まって、 せっかくキレイに作ったものをまた見直す。または壊すなどの作業が発生してしまう問題が生まれがちだと解説されていました。
確かに自分もよくあるなぁと思うところです。
ちゃんと動くところまで持っていってから/キレイに実装する の問題点
「ちゃんと動くところ」というのは、実装はどうあれ目的の動作をする。 つまりは成果物としては正常かつ正解である状態になるということです。
どちらかというとマネージメントしてる人が嬉しくなる状態なわけですが、ここでふと思うわけです。
「動くんだからキレイにしなくてもいいんじゃないの」と
要は、エンジニアとしては保守性や拡張性などを考えればリファクタリングしたくなるところですが、いかんせん動いてるんだから変更したくない。 和田さんの講演動画では「動くからいいじゃないか」という堕落。「動いてるものを触って動かなくなるかも」という怖れという表現をされていました。
TDDとは、この堕落や恐怖に打ち勝ち「動く状態」から「キレイで動く状態」に持っていく段階に用いるための開発技法(スキル)なのだということを理解しました。
結構メンタルに訴えかける開発技法なのですね。
「テスト」というけど
本の中でも言及されていますが、テスターさんのというところの「テスト」とテスト駆動開発の「テスト」は意味が異なります。
テスターさんのというところの「テスト」は、まさに成果物を壊しにかかる作業です。 言い方が悪いですが「なんとかバグなるものを晒しだしてやろう」という類のものです。 それをくぐり抜けるからこそ品質が上がると言えるわけです。
テスト駆動開発の「テスト」は、前述の「動いてるものを触って動かなくなるかも」という恐怖を取り除くための動作確認、つまり「チェック」であるという認識でいたほうが良さそうです。 つまりは、テスト駆動開発の中で生まれたテストは受け入れテストの代わりになるものではなく、エンジニアさんの安心材料のために生み出されるものだということと理解しました。
テスト駆動開発のテスト = Checking
それでも「テスト駆動開発」という名前が生み出す誤解がやはりあると、本でも述べられていますし、和田さん自身からも聞きました。 そして生まれたのが「振る舞い駆動開発(Behavior Driven Development = BDD)」なるものです。
「テスト」を「スペック」と呼ぶ・・・のように名称を変えることで、あらぬ誤解が生まれないようにした歴史も本を通じて理解しました。(付録Cに書いてあります)
ちなみに、iOS開発でテストツールとして使われるライブラリ Quick は、このBDDの考え方に準ずるものだそうです。
参考: [Swift] Quick で振る舞いテストをしてみよう
サイクルのお話
ここからは、せっかく覚えたテスト駆動開発の手法の中身について少し書きます。 概略的な話になると思うので詳しくは本をご覧になるかネット記事などを参照いただければと思います。
テスト駆動開発には3つのサイクルが存在します。
レッド → グリーン → リファクタリング
です。
これさえ覚えておけばテスト駆動開発は完璧です。(たぶん)
「ホップ・ステップ・ジャンプ」のような「サイン・コサイン・タンジェント」のような小気味よく覚えれるフレーズですね。 みなさんもここで声に出して読んでみましょう。
「レッド・グリーン・リファクタリング」
レッド
テスト駆動開発は「レッド」から始まります。 「レッド」とは、テストが失敗に終わる状態のことを指します。
今書いたテストが正しくエラーになることをまずは確認するところから始まるわけです。 正しくエラーになると予測したものがエラーにならなければまずは異常事態です。 テストツールすらも疑う状態であるというわけです。
レッドで大切なのは、プロダクトコード(実装コード)に一切何も書かれていなくても、とにかく先にテストを書くこと。そしてテストが失敗することです。
グリーン
「レッド」が終わると、「レッド」から「グリーン」にするという作業に入ります。 「グリーン」とは、テストが成功に終わる状態を指します。
ただし「テストが成功に終わる状態」であれば何でもよく、 プロダクトコード側にマジックナンバーや固定値、茶番のようなソースコード、どんな手段であってもとにかくテストを成功させます。
例えば、テストは「trueになること」を期待していれば、 テスト対象のメソッドが何も考えずにtrueを返してやればテストはグリーンです。
プロダクトコードを書き終えるまでに、「そんなクラスはないよ」とか「そんなプロパティなんてないよ」とか、 コンパイラやIDE、テストツールに色々と怒られる「レッド」な状態を経て、ようやくテストが成功する段階まで持っていくことになると思います。 この状態まで持ってこられれば「グリーン」はまさにOKなわけです。
リファクタリング
めでたく「グリーン」でテストは成功する。いやしかし、そのままでは何の役にも立たない。 これをテストが成功する状態のまま「意味のあるソースコード」に持っていく。 それが「リファクタリング」のフェーズです。
「グリーン」の時の例でいえば、trueを返すだけのメソッドなんて「意味がない」わけで、 これをちゃんと意味のあるメソッドに修正していくわけです。
ここで大事なのは、リファクタリングをもってTDDのサイクルが完結するということです。 サイクルを完結させぬままに「動いたんだから大丈夫」とグリーンのまま放置してはいけません。
和田さんの講演動画で分かりやすかったのは 「部屋から何かを引っ張り出してきたが、その都度片付けないと後で大掃除をすることになってしまう」という喩えでした。
「あとでキレイにする」は悪魔の囁きです。その都度リファクタリングは完成させたほうがよさそうです。
歩幅のお話
本では「歩幅」という言葉が何度も出てきます。 「テストファースト」から実装を組み立てていくTDDでは、どれくらいのステップ(歩幅)を踏んでいくのかが重要視されています。
先述の話でいえば、あまりにもサイクルが大きいと片付けられなくなってしまいます。毎回毎回が大掃除だと大変です。 伏線を張りすぎて物語が回収できず中途半端に終ってしまうドラマのようです。
TODOリスト
人が歩くというのは、歩幅の大小あれど目的地があるわけです。 TDDでは目的地の設定をしていくことが大事になるとのことです。
なので、まずはどんなテストが必要かを考えて TODO(やりたいこと/必要なもの)リストを作るところからスタートし、自分の目的地をちゃんと決めておくわけですね。
本でも第一章は「多国通貨」の例が挙げられましたが、そこでもTODOリストを作るところから始まりました(詳しくは本を読んで下さい)。
TODOリスト作りでのポイントは以下になることも理解しました。
- 命題を要素に分けてみる
- 命題に書かれていない正常系が隠れていないかを見つける
- テスト可能なサイズに落とし込む
- 「ただし」のあとは異常系(例外)または準正常系
- 優先順位に応じて順序を入れ替える
和田さんの講演動画では、有名なFizzBuzz問題の命題からTODOリストに落とし込む方法がすごく腑に落ちたので、 勝手ながら例として挙げさせていただくと
1〜100までの数をプリントしなさい。ただし3の倍数では「Fizz」を、5の倍数では「Buzz」を、3と5の倍数では「FizzBuzz」をプリントすること
これをまずざっくりと要素に分けると
- 1〜100までの数をプリントしなさい。
- ただし3の倍数では「Fizz」を、
- 5の倍数では「Buzz」を、
- 3と5の倍数では「FizzBuzz」をプリントすること
まず、ここから「隠れている正常系」を見つけるならば、所定の条件外では「数は、その数のままプリントされる」が含まれていることがわかります。
また、「プリントする」という動作はすべて同じなので、要素からは独立させ、仕様通りの文字列を返すテストの優先順位を上げてやります。
こういうふうに内容をやりかえてみると、下記のようなTODOリストが出来あがります。
- 数を文字列として返す
- 数が3の倍数の場合は「Fizz」を返す
- 数が5の倍数の場合は「Buzz」を返す
- 数が3と5の倍数の場合は「FizzBuzz」を返す
- 1〜100までの数を繰り返す
- プリントする
書かなければならないテストが見えてくる気がしませんか?
TODO作りは経験則なども必要で、最初からここまで上手に列挙できないかもしれません(自分もできなさそう)。 でも、このあたりはやっていくと慣れてくるような気もします。 (そのための失敗は繰り返していいと思います。TODOリストが途中で書き換わることは本の中でも許容されています)
仮実装
先述の「グリーン」の項にて、「グリーン」に持っていくために「どんな手段であってもとにかくテストを成功させる」ということを書きました。
このことを「仮実装」と呼びます。
テストコード
it("男性ユーザに男性かどうかを取得するとtrueになること") { let user = User(sex: "man") expect(user.isMan).to(beTrue()) }
プロダクトコード
class User { init(sex: String) { } var isMan: Bool { return true } }
こんな感じで書けばテストは必ず通ります。グリーンなのですから、あとはリファクタリングをすればいいわけです。
三角測量
リファクタリングの段階になると、書いたプロダクトコードに対してテストがその内容をきちんとチェックできているかどうかがわからない状態になります。 先述の仮実装で出した例に沿うと「では、女性ユーザに男性かどうかを取得しようとするとfalseになるよね?」という感じです。 そこで同じ対象に対して違う観点のテストを追記して、プロダクトコードがきちんと動作するかどうかを測ることになります。
これを「三角測量」と言います。
三角測量もまた「レッド → グリーン → リファクタリング」の段階を踏みます。
たとえば
it("男性ユーザに男性かどうかを取得するとtrueになること") { let user = User(sex: "man") expect(user.isMan).to(beTrue()) } it("女性ユーザに男性かどうかを取得するとfalseになること") { let user = User(sex: "woman") expect(user.isMan).to(beFalse()) }
先ほどのテストコードに新たにテストを書きます。
すると、新たなテストは失敗します。なぜなら、プロパティisMan
は true しか返さないからです。
予想したとおりエラーになる段階「レッド」に至ったわけです。
次にグリーンに持っていきます
プロダクトコード
class User { let sex: String init(sex: String) { self.sex = sex } var isMan: Bool { return sex == "man" } }
メンバ変数が "man" ならば true を返す・・・というメソッドにすることで、両方のテストをグリーンに持っていくことができます。
しかし、これでは "otoko" とか、"male" とかを渡しても女性扱いです。 「そもそも文字列を渡す設計が良くないのではないか?」 「性別はenum化したほうがよくない?」と実装を最適化していく「リファクタリング」に入っていくわけです。
三角関数とは、1つのテストでカバーできない部分を2つめのテストでカバーするということですね。
明白な実装
最初に「歩幅が重要」と書きました。 仮実装や三角測量はどういうときに使うかというと実装に自信がないときです。 つまり、歩幅を小さくして実装の粒度を細かくしながら先に進めていく時に使う手法です。 TDDでは全てのパターンで仮実装や三角測量を使う必要はないとされています。 「こう実装すれば絶対にうまくいく」と浮かんでいる段階でわざわざ仮実装しなくても、その実装を落とし込めばいいのです。
それを「明白な実装」と呼びます。
先ほどの例でいえば、
enum Sex { case man case woman } class User { let sex: Sex init(sex: Sex) { self.sex = sex } var isMan: Bool { return sex == .man } }
「enumを使えば、男と女に切り分けられるよね」と頭にポンと浮かべば、最初からこの実装にしても構わないということです。
ただし、テストは先に書きましょう。(enumの定義を先にしないように!)
it("男性ユーザに男性かどうかを取得するとtrueになること") { let user = User(sex: .man) expect(user.isMan).to(beTrue()) }
そしてレッドから始めるという前提は崩さないように・・・。TDDのサイクルはあくまで守ります。
また、「明白な実装」を使う時は「自信のある時」ですが、 自信があっても「あれ? ここって考慮漏れがあるかもしれない」など、その自信は次第になくなることもあります。 その場合は、仮実装や三角測量を用いてやり直していくことになります。「歩幅は常に調整される」と本の中でも実践されています。
重複の除去
「キレイにすること = リファクタリング」なのですが、「そもそもキレイって何だ」という話になると思います。
わかりやすいクラス名やメソッド名や変数名。必要な箇所が適切に隠蔽されている。美しいアーキテクチャにデザインパターン。 当然それらも大事ですが、TDDの本では「重複を除去すること」が強く書かれています。
書いたテストがプロジェクトに残っていたとして、それが必要なテストなのか不要なのかを他人が判断するのが難しい、または時間がかかる作業になります。 だから、不要なのに延々とテストが秘伝のタレのように延命し続けてしまうという事態も起きがちになります。
TDDでテストが重複するケースとしては、三角測量をしたときが多くなるかと思います。 例えば、講演動画ではFizzBuzzについてのテストの例がありました。(講演ではJavaでしたが、下記はSwiftに書き換えてます)
it("1を渡したら'1'という文字列が返ること") { expect(FizzBuzz().convert(1)).to(equal("1")) } it("2を渡したら'2'という文字列が返ること") { expect(FizzBuzz().convert(2)).to(equal("2")) } it("3を渡したら'Fizz'という文字列が返ること") { expect(FizzBuzz().convert(3)).to(equal("Fizz")) }
このテストはプロダクトコードが下記のように仮実装の段階であれば三角測量の効果がありました。
class FizzBuzz { func convert(_ num: Int) -> String { return "1" } }
2を渡すとレッドになるので「さあ、グリーンにしよう」「リファクタリングしよう」となるわけです。
しかし、リファクタリングが進み、プロダクトコードにFizzBuzzの実装ができてくるにつれて、 1を渡すパターンと2を渡すパターンはテストが重複してくることがわかります。
class FizzBuzz { func convert(_ num:Int) -> String { if num % 3 == 0 && num % 5 == 0 { return "FizzBuzz" } else if num % 3 == 0 { return "Fizz" } else if num % 5 == 0 { return "Buzz" } return "\(num)" } }
it("1を渡したら'1'という文字列が返ること") { expect(FizzBuzz().convert(1)).to(equal("1")) } it("2を渡したら'2'という文字列が返ること") { // ← このテスト、もはや要らなくない? expect(FizzBuzz().convert(2)).to(equal("2")) } it("3を渡したら'Fizz'という文字列が返ること") { expect(FizzBuzz().convert(3)).to(equal("Fizz")) }
そこで不要なテストはガッツリ消してしまいます。 これが「重複の除去」です。 プロダクトコードも、テストコードも重複していれば削除・共通化などをしてどんどんとダイエットさせていくことが大事になります。
「テストは増やすよりも減らすのが難しい」といいます。 なので、テストを書く人はテストを最小限まで減らす責任があるといえます。 テスト駆動開発におけるリファクタリングは、テストを構造化したり、減らすところまでがゴールとなるわけです。
その他まとめ
本を読む順番
読書勉強会では、第1部の「多国通貨のサンプル」をみんなで読書してそれぞれに写経をしていきながら進めていきました。 その後は、時間などの都合もあり、和田さんの書き下ろしである「付録C」まで飛ばすこととなりました。
しかし、この読み方は正解だったなぁと思いました。
第1部にはコードと解説が書かれているので、よくわからなくてもとりあえず最後まで読んでは写経する。 そして付録CでTDDの過去・現在・未来を知る。 すると、もう一度第1部から読み返してみると、最初とは違う感想を抱くはずです。
当然、第2部も第3部も読むべきだと思いますが、付録Cはなんだかジーンとするお話なので、先に読んでて損はないと思います。 (社内でのふりかえりでは「とにかくエモい」と話題になりました)
第1部 → 付録C → もう一回第1部 → 第3部 → あとはお好きに・・・
アサーションルーレット
アサーションをひとつのテストに何個も書くのはアンチパターンであるということも理解をしました。
ひとつのテストにアサーションを複数書くと、 最初のアサーションがエラーであると次以降のアサーションは評価されずにテストが終わってしまい、 TDDのサイクルを正常に回すことができなくなるというのが理由です。
このアンチパターンを「アサーションルーレットアンチパターン」と呼んでいました。
it("数を渡したらその数の文字列が返ること") { let fizzBuzz = FizzBuzz() expect(fizzBuzz.convert(1)).to(equal("1")) expect(fizzBuzz.convert(2)).to(equal("2")) }
こうではなく
context("数を渡したらその数の文字列が返ること") { it("1を渡したら'1'という文字列が返ること") { expect(FizzBuzz().convert(1)).to(equal("1")) } it("2を渡したら'2'という文字列が返ること") { expect(FizzBuzz().convert(2)).to(equal("2")) } }
これが好ましいということですね。
人によっては当たり前だろうと思う人もいるかもしれませんが、 やってしまってる人もいるかもしれないので書き留めておきます。
最初に選ぶTODO
TODOリストを作った段階で、最初にテストを書く時は「一番小さそうなものを選ぶ」のがいいそうです。 それは、まだクラスもなにも存在しない無の世界から新たに作り出す作業になるので、考えることが多すぎるからだそうです。 いきなり命題の本質を作り出すようなことをせず、部品から作り出す順序のほうがよさそうですね。
仲間を作るには
自分が和田さんが訪問されて直接質問したことは「興味ある友人などに、どうやったらTDDの魅力を伝えられますか?」でした。
それに対して「ハイリスク・ハイリターンだけど、ライブコーディングが一番」という答えをいただきました。
今回のブログでは、直接お会いした和田さんの話以上に、和田さんの講演動画に影響されて書いた内容が多くなりました。 それは「動画にライブコーディングがあったから」です。
たしかに、こうやって文章でツラツラと書いていくよりも ライブコーディングがTDDの流れが一番伝わるかもしれません。
まずは仲間をつのって、自分たちの得意な言語でテスト駆動開発の簡単なサンプルから始めてみるのはどうでしょうか。
最後に
テスト駆動開発の使いどころは、まだまだ自分も模索中です。 自分はモバイルアプリの開発が中心なので、さらに使いどころは迷うところです。
盲目的に「テスト駆動開発は素晴らしいから絶対に開発に取り入れるんだ!」というよりは 「もしかしたらこのロジカルな実装の場面では、有利なんじゃない?」と選択肢の一つに加えたいところです。
本も最後は「TDDはスキル」であり、「道を照らす明かり」であると書かれています。
自分のスタイルのひとつになるくらいに磨き上げられるように触れていこうかなと思うところです。
以上です。